package edu.buaa.energy.server;

import com.alibaba.fastjson.JSON;
import com.google.common.base.Preconditions;
import edu.buaa.common.RuntimeEnv;
import edu.buaa.common.server.DBSocketServer;
import edu.buaa.common.transaction.AbstractTransaction;
import edu.buaa.common.transaction.AbstractTransaction.Result;
import edu.buaa.common.transaction.TransactionFailedException;
import edu.buaa.energy.EnergyDataTxGenerator;
import edu.buaa.energy.transaction.*;
import edu.buaa.common.transaction.CreateTGraphAggrMaxIndexTx;
import edu.buaa.common.transaction.CreateTGraphTemporalValueIndexTx;
import edu.buaa.utils.Helper;
import edu.buaa.utils.Pair;
import edu.buaa.utils.Triple;
import org.act.temporalProperty.index.IndexType;
import org.act.temporalProperty.index.value.IndexMetaData;
import org.act.temporalProperty.query.TimePointL;
import org.act.temporalProperty.query.aggr.AggregationIndexQueryResult;
import org.act.temporalProperty.query.aggr.AggregationQuery;
import org.act.temporalProperty.query.aggr.ValueGroupingMap;
import org.act.temporalProperty.util.Slice;
import org.neo4j.graphdb.*;
import org.neo4j.temporal.*;
import org.neo4j.tooling.GlobalGraphOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

import static org.act.temporalProperty.index.IndexType.AGGR_DURATION;
import static org.act.temporalProperty.index.IndexType.AGGR_MAX;

public class TGraphKernelServer extends DBSocketServer.ReqExecutor {
    private static final Logger log = LoggerFactory.getLogger(TGraphKernelServer.class);

    public TGraphKernelServer(boolean useIndex) {
        this.useIndex = useIndex;
    }

    public static void main(String[] args){
        boolean index = !"false".equalsIgnoreCase(System.getenv("NO_INDEX"));
        DBSocketServer server = new DBSocketServer( dbDir(), new TGraphKernelServer(index), index?8438:8439);
        RuntimeEnv env = RuntimeEnv.getCurrentEnv();
        String serverCodeVersion = env.name() + "." + Helper.codeGitVersion();
        System.out.println("server code version: "+ serverCodeVersion);
        try {
            server.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static File dbDir(){
        String path = Helper.mustEnv("DB_PATH");
        Preconditions.checkNotNull(path, "need arg: DB_PATH");
        File dbDir = new File(path);
        if( !dbDir.exists()){
            if(dbDir.mkdirs()) return dbDir;
            else throw new IllegalArgumentException("invalid dbDir");
        }else if( !dbDir.isDirectory()){
            throw new IllegalArgumentException("invalid dbDir");
        }
        return dbDir;
    }

    public enum EdgeType implements RelationshipType {
        EDGE_TO
    }

    private final Map<Integer, Long> idMap2Long = new HashMap<>();
    private long aggrMaxIndexId = -1;
    private long aggrDurIndexId = -1;
    private Triple<Integer, Integer, Integer> tvCondIndex = null;
    private final boolean useIndex;

    private long id(int rawId){
        Long r = idMap2Long.get(rawId);
        if(r==null){
            throw new NullPointerException("got raw id "+rawId+" which not exist in db.");
        }
        return r;
    }

    @Override
    public void setDB(GraphDatabaseService db){
        this.db = db;
        try(Transaction tx = db.beginTx()){
            for(Node node: GlobalGraphOperations.at(db).getAllNodes()){
                Integer rawId = (Integer) node.getProperty("id");
                Preconditions.checkNotNull(rawId,"should not happen: id is null");
                idMap2Long.put(rawId, node.getId());
//                System.out.println(rawId+" , "+ node.getId());
            }
            for(IndexMetaData m : db.temporalIndex().nodeIndexes()){
                System.out.println(m);
                if(m.isOnline()) {
                    if (m.getType() == AGGR_DURATION) aggrDurIndexId = m.getId();
                    if (m.getType() == AGGR_MAX) aggrMaxIndexId = m.getId();
                    if (m.getType() == IndexType.SINGLE_VALUE ){
                        tvCondIndex = Triple.of(m.getPropertyIdList().get(0), m.getTimeStart().valInt(), m.getTimeEnd().valInt());
                    }
                }
            }
            System.out.println("AGGR_DUR_INDEX_ID: "+aggrDurIndexId);
            System.out.println("AGGR_MAX_INDEX_ID: "+aggrMaxIndexId);
            System.out.println("TIME_VAL_INDEX: "+tvCondIndex);
            tx.success();
        }
    }

    @Override
    public Result execute(String line) throws RuntimeException {
        AbstractTransaction tx = JSON.parseObject(line, AbstractTransaction.class);
        try {
            switch (tx.getTxType()) {
                case tx_import_static_data:
                    return execute((ImportStaticDataTx) tx);
                case tx_import_temporal_data:
                    return execute((ImportTemporalDataTx) tx);
                case tx_update_temporal_data:
                    return execute((UpdateTemporalDataTx) tx);
                case tx_query_snapshot:
                    return execute((SnapshotQueryTx) tx);
                case tx_query_entity_history:
//                    System.out.println(line);
                    return execute((EntityHistoryTx) tx);
                case tx_query_road_by_temporal_condition:
                    EntityTemporalConditionTx etx = (EntityTemporalConditionTx) tx;
//                    int tpId = db.temporalIndex().proName2Id(etx.getP());
                    if(useIndex && tvCondIndex!=null && etx.getP().equals(EnergyDataTxGenerator.properties[0]) &&
                            tvCondIndex.getMiddle() <= etx.getT0() && etx.getT1() <= tvCondIndex.getRight()){
                        return execute(etx, 0);
                    } else{
                        return execute(etx);
                    }
                case tx_query_snapshot_aggr_max:
                    if (aggrMaxIndexId < 0 || !useIndex) return execute((SnapshotAggrMaxTx) tx);
                    else return execute((SnapshotAggrMaxTx) tx, aggrMaxIndexId);
                case tx_query_snapshot_aggr_duration:
                    if (aggrDurIndexId < 0 || !useIndex) return execute((SnapshotAggrDurationTx) tx);
                    else return execute((SnapshotAggrDurationTx) tx, aggrDurIndexId);

                case tx_index_tgraph_aggr_max:
                    return execute((CreateTGraphAggrMaxIndexTx) tx);
                case tx_index_tgraph_aggr_duration:
                    return execute((CreateTGraphAggrDurationIndexTx) tx);
                case tx_index_tgraph_temporal_condition:
                    return execute((CreateTGraphTemporalValueIndexTx) tx);
                default:
                    throw new UnsupportedOperationException();
            }
        } catch (Exception e) {
            int end = Math.min(60, line.length());
            log.error("ERROR processing TX: "+line.substring(0, end), e);
            throw new TransactionFailedException(e);
        }
    }

    private Result execute(ImportStaticDataTx tx){
        try(Transaction t = db.beginTx()) {
            for (ImportStaticDataTx.EnergyNode node : tx.getNodes()) {
                Node n = db.createNode();
                n.setProperty("id", node.getRawId());
                n.setProperty("name", node.getName());
                n.setProperty("country", node.getCountry());
                n.setProperty("voltage", node.getVoltage());
                n.setProperty("latitude", node.getLatitude());
                n.setProperty("longitude", node.getLongitude());
                idMap2Long.put(node.getRawId(), n.getId());
            }
            for (ImportStaticDataTx.EnergyRel sr : tx.getRels()) {
                Node start = db.getNodeById(id(sr.getFromRawId()));
                Node end = db.getNodeById(id(sr.getToRawId()));
                Relationship r = start.createRelationshipTo(end, EdgeType.EDGE_TO);
                r.setProperty("length", sr.getLength());
                r.setProperty("x", sr.getX());
                r.setProperty("y", sr.getY());
                r.setProperty("limit", sr.getLimit());
            }
            t.success();
        }
        return new Result();
    }

    private Result execute(ImportTemporalDataTx tx) {
        try(Transaction t = db.beginTx()) {
            for(ImportTemporalDataTx.EnergyStatus s : tx.data){
                Node n = db.getNodeById(id(s.getNodeId()));
                TimePoint time = Helper.time(s.getTime());
                setProperties(s.getStatus(), n, time, null);
            }
            t.success();
        }
        return new Result();
    }

    private void setProperties(List<Float> values, Node node, TimePoint begin, TimePoint end){
        for(int i = 0; i< EnergyDataTxGenerator.properties.length; i++) {
            String propName = EnergyDataTxGenerator.properties[i];
            Float val = values.get(i);
            if(end==null){
                node.setTemporalProperty(propName, begin, val);
            }else{
                node.setTemporalProperty(propName, begin, end, val);
            }
        }
    }

    private Result execute(UpdateTemporalDataTx tx) {
        try(Transaction t = db.beginTx()) {
            Node n = db.getNodeById(id(tx.getEntityId()));
            setProperties(tx.getValues(), n, Helper.time(tx.getStartTime()), Helper.time(tx.getEndTime()));
            t.success();
        }
        return new Result();
    }

    private Result execute(SnapshotQueryTx tx){
        try(Transaction t = db.beginTx()) {
            List<Pair<Integer, Float>> answers = new ArrayList<>();
            for(Node n: GlobalGraphOperations.at(db).getAllNodes()){
                Integer id = (Integer) n.getProperty("id");
                Object v = n.getTemporalProperty(tx.getPropertyName(), Helper.time(tx.getTimestamp()));
                if(v!=null){
                    answers.add(Pair.of(id, (Float) v));
                }
            }
            SnapshotQueryTx.Result result = new SnapshotQueryTx.Result();
            result.setStatus(answers);
            return result;
        }
    }

    private Result execute(SnapshotAggrMaxTx tx){
        try(Transaction t = db.beginTx()){
            List<Pair<Integer, Float>> answers = new ArrayList<>();
            for (Node n : GlobalGraphOperations.at(db).getAllNodes()){
                Integer id = (Integer) n.getProperty("id");
                Object v = n.getTemporalProperty(tx.getP(), Helper.time(tx.getT0()), Helper.time(tx.getT1()), new TemporalRangeQuery.MaxFloat());
                if(v!=null){
                    answers.add(Pair.of(id, (Float) v)); //放入answers中
                }
            }
            SnapshotAggrMaxTx.Result result = new SnapshotAggrMaxTx.Result();
            result.setPropMaxValue(answers);
            return result;
        }
    }

    private Result execute(SnapshotAggrDurationTx tx) {
        TimePoint begin = Helper.time(tx.getT0()), end = Helper.time(tx.getT1());
        try (Transaction t = db.beginTx()) {
            List<Triple<Integer, Float, Integer>> answers = new ArrayList<>();
            for (Node n : GlobalGraphOperations.at(db).getAllNodes()) {
                Integer entity = (Integer) n.getProperty("id");
                Object v = n.getTemporalProperty(tx.getP(), begin, end, new TimeIntervalRangeQuery.FloatDuration(begin, end, tx.getIntStartTreeSet()));
                if(v instanceof HashMap){
                    HashMap<Float, Integer> m = (HashMap<Float, Integer>) v;
                    m.forEach((k, val) -> answers.add(Triple.of(entity, k, val)));
                }
            }
            SnapshotAggrDurationTx.Result result = new SnapshotAggrDurationTx.Result();
            result.setStatusDuration(answers);
            return result;
        }
    }

    private Result execute(EntityHistoryTx tx) {
        TimePoint begin = Helper.time(tx.getBeginTime()), end = Helper.time(tx.getEndTime());
        try (Transaction t = db.beginTx()) {
            List<Triple<Integer, Integer, Float>> answers = new ArrayList<>();
            Node n = db.getNodeById(id(tx.getEntity()));
            n.getTemporalProperty(tx.getProp(), begin, end, new TimeIntervalRangeQuery(begin, end){
                @Override
                public void onEntry(TimePointL beginTime, TimePointL endTime, Object val) {
                    if(val instanceof Float) {
                        answers.add(Triple.of(beginTime.valInt(), endTime.valInt(), (Float) val));
                    }
                }
                @Override public Object returnValue() { return null; }
            });
            EntityHistoryTx.Result result = new EntityHistoryTx.Result();
            result.setHistory(answers);
            return result;
        }
    }

    private Result execute(EntityTemporalConditionTx tx) {
        try (Transaction t = db.beginTx()) {
            List<Integer> res = new ArrayList<>();
            for (Node n : GlobalGraphOperations.at(db).getAllNodes()) {
                int entity = (Integer) n.getProperty("id");
                n.getTemporalProperty(tx.getP(), Helper.time(tx.getT0()), Helper.time(tx.getT1()), new TemporalRangeQuery() {
                    @Override
                    public boolean onNewEntry(long entityId, int propertyId, TimePointL time, Object val) {
                        if(val instanceof Float && tx.getVMin() <= ((Float) val) && ((Float) val) <= tx.getVMax()){
                            res.add(entity);
                            return false;
                        }else{
                            return true;
                        }
                    }
                    @Override
                    public Object onReturn() {
                        return res;
                    }
                });
            }
            EntityTemporalConditionTx.Result result = new EntityTemporalConditionTx.Result();
            result.setEntities(res);
            return result;
        }
    }

//    ====================== index: create and query =======================

    private Result execute(CreateTGraphTemporalValueIndexTx tx)  {
        long indexId = -1;
        try(Transaction t = db.beginTx()){
            indexId = db.temporalIndex().nodeCreateValueIndex(Helper.time(tx.getStart()), Helper.time(tx.getEnd()), tx.getProps().toArray(new String[]{}));
            t.success();
        }
        while(true) try {
            db.temporalIndex().awaitIndexOnline(indexId);
            break;
        } catch (InterruptedException e) {
            Thread.interrupted();
        }
        CreateTGraphTemporalValueIndexTx.Result r = new CreateTGraphTemporalValueIndexTx.Result();
        r.setIndexId(indexId);
        return r;
    }

    private Result execute(CreateTGraphAggrDurationIndexTx tx) {
        long indexId = -1;
        try(Transaction t = db.beginTx()){
            indexId = db.temporalIndex().nodeCreateDurationIndex(
                    Helper.time(tx.getStart()), Helper.time(tx.getEnd()),
                    tx.getProName(), tx.getEvery(), tx.getTimeUnit(), new ValueGroupingMap.FloatRangeGroupMap(tx.getIntervalStart()));
            t.success();
        }
        while(true) try {
            db.temporalIndex().awaitIndexOnline(indexId);
            break;
        } catch (InterruptedException e) {
            Thread.interrupted();
        }
        CreateTGraphAggrDurationIndexTx.Result r = new CreateTGraphAggrDurationIndexTx.Result();
        r.setIndexId(indexId);
        aggrDurIndexId = indexId;
        return r;
    }

    private Result execute(CreateTGraphAggrMaxIndexTx tx) {
        long indexId = -1;
        try(Transaction t = db.beginTx()){
            indexId = db.temporalIndex().nodeCreateMinMaxIndex(
                    Helper.time(tx.getStart()), Helper.time(tx.getEnd()),
                    tx.getProName(), tx.getEvery(), tx.getTimeUnit(), AGGR_MAX);
            t.success();
        }
        while(true) try {
            db.temporalIndex().awaitIndexOnline(indexId);
            break;
        } catch (InterruptedException e) {
            Thread.interrupted();
        }
        CreateTGraphAggrMaxIndexTx.Result r = new CreateTGraphAggrMaxIndexTx.Result();
        r.setIndexId(indexId);
        aggrMaxIndexId = indexId;
        return r;
    }

    private Result execute(SnapshotAggrMaxTx tx, long indexId){
//        System.out.println("index query(" + tx.getT0() +"~"+tx.getT1()+") via max index");
        try(Transaction t = db.beginTx()){
            List<Pair<Integer, Float>> answers = new ArrayList<>();
            for (Node node : GlobalGraphOperations.at(db).getAllNodes()){
                Integer id = (Integer) node.getProperty("id");
                AggregationIndexQueryResult v = node.getTemporalPropertyWithIndex(tx.getP(), Helper.time(tx.getT0()), Helper.time(tx.getT1()), indexId);
                if(v!=null){
                    Map<Integer, Slice> result = v.getMinMaxResult();
                    answers.add(Pair.of(id, result.get(AggregationQuery.MAX).input().readFloat())); // 0 min, 1 max
                }
            }
            SnapshotAggrMaxTx.Result result = new SnapshotAggrMaxTx.Result();
            result.setPropMaxValue(answers);
            return result;
        }
    }

    private Result execute(SnapshotAggrDurationTx tx, long indexId) {
//        System.out.println("index query(" + tx.getT0() +"~"+tx.getT1()+") via dur index");
        try (Transaction t = db.beginTx()) {
            List<Triple<Integer, Float, Integer>> answers = new ArrayList<>();
            for (Node node : GlobalGraphOperations.at(db).getAllNodes()) {
                Integer id = (Integer) node.getProperty("id");
                AggregationIndexQueryResult v = node.getTemporalPropertyWithIndex(tx.getP(), Helper.time(tx.getT0()), Helper.time(tx.getT1()), indexId);
                if(v!=null){
                    Map<Float, Integer> result = v.getDurationWithMapping();
                    result.forEach((k, val)-> answers.add(Triple.of(id, k, val)));
                }
            }
            SnapshotAggrDurationTx.Result result = new SnapshotAggrDurationTx.Result();
            result.setStatusDuration(answers);
            return result;
        }
    }

    private Result execute(EntityTemporalConditionTx tx, long indexId){
        System.out.println("query(" + tx.getT0() +"~"+tx.getT1()+" "+ "via index("+tvCondIndex.getMiddle()+"~"+tvCondIndex.getRight()+")");
        List<Integer> answersFinal;
        try(Transaction t = db.beginTx()){
            TemporalIndexManager.PropertyValueIntervalBuilder query = db.temporalIndex().nodeQueryValueIndex(Helper.time(tx.getT0()), Helper.time(tx.getT1()));
            query.propertyValRange(tx.getP(), tx.getVMin(), tx.getVMax());
            List<IntervalEntry> answers = query.query();
            answersFinal = answers.stream().map(IntervalEntry::getEntityId).distinct().map(id->{
                Node node = db.getNodeById(id);
                return (Integer) node.getProperty("id");
            }).collect(Collectors.toList());
        }
        EntityTemporalConditionTx.Result result = new EntityTemporalConditionTx.Result();
        result.setEntities(answersFinal);
        return result;
    }
}
